import { traceError, traceVerbose } from "../../logging";
import { isTestExecution } from "../constants";
import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from "./cacheUtils";
import { StopWatch } from "./stopWatch";

const _debounce = require('lodash/debounce') as typeof import('lodash/debounce');
type VoidFunction = () => any;
type PromiseFunctionWithAnyArgs = (...any: any) => Promise<any>;
const cacheStoreForMethods = getGlobalCacheStore();
const moduleLoadWatch = new StopWatch();
const extensionStartUpTime = 200_000;
export function cache(expiryDurationMs: number, cachePromise = false, expiryDurationAfterStartUpMs?: number) {
    return function (
        target: Object,
        propertyName: string,
        descriptor: TypedPropertyDescriptor<PromiseFunctionWithAnyArgs>,
    ) {
        const originalMethod = descriptor.value!;
        const className = 'constructor' in target && target.constructor.name ? target.constructor.name : '';
        const keyPrefix = `Cache_Method_Output_${className}.${propertyName}`;
        descriptor.value = async function (...args: any) {
            if (isTestExecution()) {
                return originalMethod.apply(this, args) as Promise<any>;
            }
            let key: string;
            try {
                key = getCacheKeyFromFunctionArgs(keyPrefix, args);
            } catch (ex) {
                traceError('Error while creating key for keyPrefix:', keyPrefix, ex);
                return originalMethod.apply(this, args) as Promise<any>;
            }
            const cachedItem = cacheStoreForMethods.get(key);
            if (cachedItem && (cachedItem.expiry > Date.now() || expiryDurationMs === -1)) {
                traceVerbose(`Cached data exists ${key}`);
                return Promise.resolve(cachedItem.data);
            }
            const expiryMs =
                expiryDurationAfterStartUpMs && moduleLoadWatch.elapsedTime > extensionStartUpTime
                    ? expiryDurationAfterStartUpMs
                    : expiryDurationMs;
            const promise = originalMethod.apply(this, args) as Promise<any>;
            if (cachePromise) {
                cacheStoreForMethods.set(key, { data: promise, expiry: Date.now() + expiryMs });
            } else {
                promise
                    .then((result) => cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryMs }))
                    .ignoreErrors();
            }
            return promise;
        };
    };
}

export function debounceSync(wait?: number) {
    if (isTestExecution()) {
        // If running tests, lets debounce until the next cycle in the event loop.
        // Same as `setTimeout(()=> {}, 0);` with a value of `0`.
        wait = undefined;
    }
    return makeDebounceDecorator(wait);
}

export function makeDebounceDecorator(wait?: number) {
    return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<VoidFunction>) {
        // We could also make use of _debounce() options.  For instance,
        // the following causes the original method to be called
        // immediately:
        //
        //   {leading: true, trailing: false}
        //
        // The default is:
        //
        //   {leading: false, trailing: true}
        //
        // See https://lodash.com/docs/#debounce.
        const options = {};
        const originalMethod = descriptor.value!;
        const debounced = _debounce(
            function (this: any) {
                return originalMethod.apply(this, arguments as any);
            },
            wait,
            options,
        );
        (descriptor as any).value = debounced;
    };
}

export function swallowExceptions(scopeName?: string) {
    return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) {
        const originalMethod = descriptor.value!;
        const errorMessage = `EQ Extension (Error in ${scopeName || propertyName}, method:${propertyName}):`;

        descriptor.value = function (...args: any[]) {
            try {
                const result = originalMethod.apply(this, args);

                // If method being wrapped returns a promise then wait and swallow errors.
                if (result && typeof result.then === 'function' && typeof result.catch === 'function') {
                    return (result as Promise<void>).catch((error) => {
                        if (isTestExecution()) {
                            return;
                        }
                        traceError(errorMessage, error);
                    });
                }
            } catch (error) {
                if (isTestExecution()) {
                    return;
                }
                traceError(errorMessage, error);
            }
        };
    };
}